#pragma once

#include <cstdint>
#include <limits>
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>

#include "planner/group_expression.h"
#include "planner/optimizer_defs.h"
#include "planner/property_set.h"

extern "C" {
#include <nodes/pathnodes.h>
}

namespace optimizer {

/**
 * Group collects together GroupExpressions that represent logically
 * equivalent expression trees.  A Group tracks both logical and
 * physical GroupExpressions.
 */
class Group {
 public:
  /** Value for uninitialized stats (cardinality and number of rows in table) */
  static constexpr int32_t UNINITIALIZED_NUM_ROWS = std::numeric_limits<int32_t>::max();

  struct GroupStats {
    Cardinality node_rows_ = UNINITIALIZED_NUM_ROWS;
    int32_t tuple_width_ = UNINITIALIZED_NUM_ROWS;
  };

  /**
   * Constructor for a group
   * @param id ID of the Group
   * @param table_aliases Set of table aliases used by the Group
   */
  explicit Group(group_id_t id) : id_(id) {}

  /**
   * Destructor
   * Deletes all owned pointers which includes PropertySet.
   * Deletes all GroupExpressions owned.
   */
  ~Group();

  /**
   * Adds an expression to the group
   *
   * If the GroupExpression is generated by applying a
   * property enforcer, we add them to enforced_exprs_
   * which will not be enumerated during OptimizeExpression.
   *
   * The GroupExpression will be owned by this Group
   */
  void AddExpression(GroupExpression *expr, bool enforced);

  /**
   * Sets metadata for the cost of an expression w.r.t PropertySet
   * @param expr GroupExpression whose metadata is to be updated
   * @param cost Cost
   * @param properties PropertySet satisfied by GroupExpression
   * @returns TRUE if expr recorded
   *
   * @note properties becomes owned by Group!
   * @note properties lifetime after not guaranteed
   */
  bool SetExpressionCost(GroupExpression *expr, double cost, PropertySet *properties);

  /**
   * Gets the best expression existing for a group satisfying
   * a certain PropertySet. HasExpressions() should return TRUE.
   *
   * @param properties PropertySet to use for search
   * @returns GroupExpression satisfing the PropertySet
   */
  GroupExpression *GetBestExpression(PropertySet *properties);

  std::tuple<double, GroupExpression *> GetBestExpressionWithCost(PropertySet *properties);

  /**
   * Determines whether or not a lowest cost expression exists
   * for this group that satisfies a certain PropertySet.
   *
   * @param properties PropertySet to use for search
   * @returns Boolean indicating whether expression is found
   */
  bool HasExpressions(PropertySet *properties) const;

  /**
   * Gets the vector of all logical expressions
   * @returns Logical expressions belonging to this group
   */
  const std::vector<GroupExpression *> &GetLogicalExpressions() const { return logical_expressions_; }

  /**
   * Gets the vector of all physical expressions
   *@returns Physical expressions belonging to this group
   */
  const std::vector<GroupExpression *> &GetPhysicalExpressions() const { return physical_expressions_; }

  /**
   * Gets the cost lower bound
   * @note currently not set anywhere...
   * @returns lower cost bound
   */
  double GetCostLB() const { return cost_lower_bound_; }

  /**
   * Sets a flag indicating the group has been explored
   */
  void SetExplorationFlag() { has_explored_ = true; }

  /**
   * Checks whether this group has been explored yet.
   * @returns TRUE if explored
   */
  bool HasExplored() const { return has_explored_; }

  /**
   * Add the selectivity of a filter column (multiply selectivities for the same column, assuming conjunction AND)
   * @param column_id column ID
   * @param selectivity estimated selectivity
   */
  void AddFilterColumnSelectivity(Oid column_id, double selectivity) {
    if (filter_column_selectivities_.find(column_id) == filter_column_selectivities_.end())
      filter_column_selectivities_[column_id] = selectivity;
    else
      filter_column_selectivities_[column_id] *= selectivity;
  }

  /**
   * Get the selectivities for filter columns  (selectivities multiplied for the same column, assuming conjunction AND)
   * @returns estimated selectivities
   */
  const std::unordered_map<Oid, double> &GetFilterColumnSelectivities() const { return filter_column_selectivities_; }

  /**
   * Gets this Group's GroupID
   * @returns GroupID of this group
   */
  group_id_t GetID() const { return id_; }

  /**
   * Erase the logical expression stored by this group.
   * Should only be called during rewrite phase.
   */
  void EraseLogicalExpression();

  /**
   * Gets the logical expression stored by this group.
   * Should only be called during rewrite phase.
   */
  GroupExpression *GetLogicalExpression() {
    CASCADES_ASSERT(logical_expressions_.size() == 1, "There should exist only 1 logical expression");
    CASCADES_ASSERT(physical_expressions_.empty(), "No physical expressions should be present");
    return logical_expressions_[0];
  }

  std::string ToString() const;

  GroupStats &GetGroupStats() { return group_stats_; }

  PathTarget *GetGroupTarget() const { return group_target_; }

  void SetGroupTarget(PathTarget *target) { group_target_ = target; }

 private:
  group_id_t id_;

  std::unordered_map<PropertySet *, std::tuple<double, GroupExpression *>, PropSetPtrHash, PropSetPtrEq>
      lowest_cost_expressions_;

  bool has_explored_{false};

  std::vector<GroupExpression *> logical_expressions_;

  std::vector<GroupExpression *> physical_expressions_;

  std::vector<GroupExpression *> enforced_exprs_;

  GroupStats group_stats_;

  PathTarget *group_target_{nullptr};

  double cost_lower_bound_ = -1;

  std::unordered_map<Oid, double> filter_column_selectivities_;
};

}  // namespace optimizer
